#include <graphics/BillboardSet.h>

namespace ne
{
    RadixSort32<BillboardList, Billboard*, float> BillboardSet::RadixSorter;

    BillboardSet::BillboardSet()
        : Node(),
          mBillboardType(BBT_POINT),
          mOriginType(TAT_CENTER),
          mWidth(1.0f), mHeight(1.0f),
          mPointRendering(false),
          mAccurateFacing(false),
          mUseOwnSize(false),
          mSortEnable(false),
          mCullIndividual(false),
          mUseColor(true),
          mUseTexCoord(true),
          mpVertexBufferIterator(0),
          mpIndexBufferIterator(0),
          mActiveBillboardNum(0),
          mVisibleBillboardNum(0),
          mCurrentCameraPosition(Vector3::ZERO),
          mCurrentCameraOrientation(Quaternion::IDENTITY)
    {
    }

    void BillboardSet::setBillboardType(const BillboardType bbt)
    {
        mBillboardType = bbt;
    }

    BillboardType BillboardSet::getBillboardType() const
    {
        return mBillboardType;
    }

    void BillboardSet::setOriginType(const BillboardOrigin bbo)
    {
        mOriginType = bbo;
    }

    BillboardOrigin BillboardSet::getOriginType() const
    {
        return mOriginType;
    }

    void BillboardSet::setSize(const real width, const real height)
    {
        mWidth = width;
        mHeight = height;
    }

    void BillboardSet::setWidth(const real width)
    {
        mWidth = width;
    }

    real BillboardSet::getWidth() const
    {
        return mWidth;
    }

    void BillboardSet::setHeight(const real height)
    {
        mHeight = height;
    }

    real BillboardSet::getHeight() const
    {
        return mHeight;
    }

    void BillboardSet::setPointRendering(const bool b)
    {
        if (mPointRendering != b)
        {
            _destroyBuffer();
        }
        mPointRendering = b;
    }

    bool BillboardSet::isPointRendering() const
    {
        return mPointRendering;
    }

    void BillboardSet::setAccurateFacing(const bool b)
    {
        mAccurateFacing = b;
    }

    bool BillboardSet::isAccurateFacing() const
    {
        return mAccurateFacing;
    }

    void BillboardSet::setUseOwnSize(const bool b)
    {
        mUseOwnSize = b;
    }

    bool BillboardSet::isUseOwnSize() const
    {
        return mUseOwnSize;
    }

    void BillboardSet::setSortEnable(const bool b)
    {
        mSortEnable = b;
    }

    bool BillboardSet::isSortEnable() const
    {
        return mSortEnable;
    }

    SortMode BillboardSet::getSortMode() const
    {
        if (mAccurateFacing ||
            mBillboardType == BBT_PERPENDICULAR_COMMON ||
            mBillboardType == BBT_PERPENDICULAR_SELF)
        {
            return SM_DISTANCE;
        }
        else
        {
            return SM_DIRECTION;
        }
    }

    void BillboardSet::setCullIndividually(const bool b)
    {
        mCullIndividual = b;
    }

    bool BillboardSet::isCullIndividually() const
    {
        return mCullIndividual;
    }

    void BillboardSet::setUseColor(const bool b)
    {
        if (mUseColor != b)
        {
            _destroyBuffer();
        }
        mUseColor = b;
    }

    bool BillboardSet::isUseColor() const
    {
        return mUseColor;
    }

    void BillboardSet::setUseTexCoord(const bool b)
    {
        if (mUseTexCoord != b)
        {
            _destroyBuffer();
        }
        mUseTexCoord = b;
    }

    bool BillboardSet::isUseTexCoord() const
    {
        return mUseTexCoord;
    }

    size_t BillboardSet::getVisibleBillboardNum() const
    {
        return mVisibleBillboardNum;
    }

    void BillboardSet::setBillboardPoolSize(const size_t size)
    {
        if (size > mBillboardPool.size())
        {
            _increaseBillboardPool(size);
            _destroyBuffer();
        }
    }

    size_t BillboardSet::getBillboardPoolSize() const
    {
        return mBillboardPool.size();
    }

    Billboard* BillboardSet::createBillboard()
    {
        if (!mFreeBillboards.empty())
        {
            Billboard *pBillboard = mFreeBillboards.front();
            mFreeBillboards.pop_front();
            mActiveBillboards.push_back(pBillboard);
            ++mActiveBillboardNum;
            return pBillboard;
        }
        return 0;
    }

    size_t BillboardSet::getBillboardNum() const
    {
        return mActiveBillboardNum;
    }

    Billboard* BillboardSet::getBillboard(const size_t index)
    {
        if (index < mActiveBillboardNum)
        {
            BillboardListIterator it = mActiveBillboards.begin();
            std::advance(it, index);
            return *it;
        }
        return 0;
    }

    BillboardList& BillboardSet::getBillboardList()
    {
        return mActiveBillboards;
    }

    void BillboardSet::clearBillboards()
    {
        mFreeBillboards.splice(mFreeBillboards.end(), mActiveBillboards);
        mActiveBillboardNum = 0;
    }

    GLBufferObjectPtr BillboardSet::getVertexBuffer() const
    {
        return mpVertexBuffer;
    }

    GLBufferObjectPtr BillboardSet::getIndexBuffer() const
    {
        return mpIndexBuffer;
    }

    VertexDataPtr BillboardSet::getVertexData() const
    {
        return mpVertexData;
    }

    IndexDataPtr BillboardSet::getIndexData() const
    {
        return mpIndexData;
    }

    // TODO: need to fix
    void BillboardSet::beginBillboards(const size_t numBillboards)
    {
        _createBuffer();

        if (!mPointRendering)
        {
            _genOriginOffsets(mOriginOffsets);

            if (!mAccurateFacing &&
                mBillboardType != BBT_ORIENTED_SELF &&
                mBillboardType != BBT_PERPENDICULAR_SELF)
            {
                _genBillboardAxes(mBillboardAxes[0], mBillboardAxes[1]);
                if (!mUseOwnSize)
                {
                    _genVertexOffsets(mWidth, mHeight, mOriginOffsets,
                                      mBillboardAxes[0], mBillboardAxes[1],
                                      mVertexOffsets);
                }
            }
        }

        if (mPointRendering)
        {
            mpVertexBufferIterator = static_cast<real*>(mpVertexBuffer->lock(BOA_WRITE_ONLY, 0,
                numBillboards * mpVertexData->getStride(), false));
        }
        else
        {
            mpVertexBufferIterator = static_cast<real*>(mpVertexBuffer->lock(BOA_WRITE_ONLY, 0,
                4 * numBillboards * mpVertexData->getStride(), false));
            mpIndexBufferIterator = static_cast<uint32_t*>(mpIndexBuffer->lock(BOA_WRITE_ONLY, 0,
                6 * numBillboards * VertexAttribute::GetAttributeSize(VAT_UNSIGNED_INT), false));
        }
        mVisibleBillboardNum = 0;
    }

    // TODO: need to fix
    void BillboardSet::injectBillboard(const Billboard &bb)
    {
        if (mVisibleBillboardNum >= mActiveBillboardNum) return;

        if (!mPointRendering)
        {
            if (mAccurateFacing ||
                mBillboardType == BBT_ORIENTED_SELF ||
                mBillboardType == BBT_PERPENDICULAR_SELF)
            {
                _genBillboardAxes(mBillboardAxes[0], mBillboardAxes[1], &bb);
            }

            if (mUseOwnSize)
            {
                _genVertexOffsets(bb.width, bb.height, mOriginOffsets,
                                  mBillboardAxes[0], mBillboardAxes[1], mVertexOffsets);
            }
            else
            {
                if (mAccurateFacing ||
                    mBillboardType == BBT_ORIENTED_SELF ||
                    mBillboardType == BBT_PERPENDICULAR_SELF)
                {
                    _genVertexOffsets(mWidth, mHeight, mOriginOffsets,
                                      mBillboardAxes[0], mBillboardAxes[1], mVertexOffsets);
                }
            }
        }

        _genVertices(mVertexOffsets, bb);
    }

    void BillboardSet::endBillboards()
    {
        if (mpVertexBuffer.notNull())
        {
            mpVertexBuffer->unlock();
        }
        mpVertexBufferIterator = 0;

        if (mpIndexBuffer.notNull())
        {
            mpIndexBuffer->unlock();
        }
        mpIndexBufferIterator = 0;
    }

    void BillboardSet::updateBillboards(const Camera *pCamera)
    {
        if (pCamera)
        {
            mCurrentCameraPosition = pCamera->getPosition(TS_WORLD);
            mCurrentCameraOrientation = pCamera->getOrientation(TS_WORLD);

            if (mSortEnable)
            {
                _sortBillboards(pCamera);
            }
        }

        beginBillboards(mActiveBillboardNum);

        if (mCullIndividual && pCamera)
        {
            for (BillboardListIterator it=mActiveBillboards.begin(); it!=mActiveBillboards.end(); ++it)
            {
                if (isBillboardVisible(pCamera, *(*it)))
                {
                    injectBillboard(*(*it));
                }
            }
        }
        else
        {
            for (BillboardListIterator it=mActiveBillboards.begin(); it!=mActiveBillboards.end(); ++it)
            {
                injectBillboard(*(*it));
            }
        }

        endBillboards();
    }

    bool BillboardSet::isBillboardVisible(const Camera *pCamera, const Billboard &bb)
    {
        const Frustum &frustum = pCamera->getCullingFrustum();
        Sphere sphere(bb.position, 0.0f);

        if (mUseOwnSize)
        {
            sphere.radius = std::max(bb.width, bb.height);
        }
        else
        {
            sphere.radius = std::max(mWidth, mHeight);
        }
        return frustum.intersects(sphere);
    }

    BillboardSet::~BillboardSet()
    {
        _destroyBillboardPool();
        _destroyBuffer();
    }

    void BillboardSet::_increaseBillboardPool(const size_t size)
    {
        size_t oldSize = mBillboardPool.size();
        if (oldSize < size)
        {
            mBillboardPool.reserve(size);
            mBillboardPool.resize(size);
            for (size_t i=oldSize; i<size; ++i)
            {
                mBillboardPool[i] = new Billboard();
                mFreeBillboards.push_back(mBillboardPool[i]);
            }
        }
    }

    void BillboardSet::_destroyBillboardPool()
    {
        mActiveBillboards.clear();
        mFreeBillboards.clear();
        for (BillboardVectorIterator it=mBillboardPool.begin(); it!=mBillboardPool.end(); ++it)
        {
            delete (*it);
        }
        mBillboardPool.clear();
    }

    void BillboardSet::_createBuffer()
    {
        if (mpVertexBuffer.isNull())
        {
            uint32_t poolSize = uint32_t(mBillboardPool.size());

            uint32_t stride = VertexAttribute::GetAttributeSize(VAT_FLOAT3);
            if (mUseColor)
            {
                stride += VertexAttribute::GetAttributeSize(VAT_FLOAT4);
            }
            if (mUseTexCoord)
            {
                stride += VertexAttribute::GetAttributeSize(VAT_FLOAT2);
            }

            // Create Vertex Buffer
            uint32_t vertexCount = (mPointRendering ? poolSize : poolSize * 4);
            uint32_t vertexBufferSize = stride * vertexCount;
            mpVertexBuffer = GLBufferObjectManager::GetSingleton().createBufferObject(
                BOT_ARRAY_BUFFER, BOU_DYNAMIC_DRAW, vertexBufferSize);
            mpVertexData.bind(new VertexData((mPointRendering ? PT_POINTS : PT_TRIANGLES)));

            uint32_t bufferSource = mpVertexBuffer->getSource();
            uint32_t bufferOffset = 0;
            mpVertexData->createAttribute(bufferSource, VAS_POSITION, VAT_FLOAT3, vertexCount, stride, bufferOffset);
            bufferOffset += VertexAttribute::GetAttributeSize(VAT_FLOAT3);
            if (mUseColor)
            {
                mpVertexData->createAttribute(bufferSource, VAS_COLOR, VAT_FLOAT4, vertexCount, stride, bufferOffset);
                bufferOffset += VertexAttribute::GetAttributeSize(VAT_FLOAT4);
            }
            if (mUseTexCoord)
            {
                mpVertexData->createAttribute(bufferSource, VAS_TEXTURE_COORD, VAT_FLOAT2, vertexCount, stride, bufferOffset);
                bufferOffset += VertexAttribute::GetAttributeSize(VAT_FLOAT2);
            }
            mpVertexData->setStride(stride);

            // Create Index Buffer
            if (!mPointRendering)
            {
                uint32_t indexCount = poolSize * 6;
                uint32_t indexBufferSize = VertexAttribute::GetAttributeSize(VAT_UNSIGNED_INT) * indexCount;
                mpIndexBuffer = GLBufferObjectManager::GetSingleton().createBufferObject(
                    BOT_ELEMENT_ARRAY_BUFFER, BOU_DYNAMIC_DRAW, indexBufferSize);
                mpIndexData.bind(
                    new IndexData(PT_TRIANGLES, mpIndexBuffer->getSource(),
                                  VAT_UNSIGNED_INT, indexCount, 0));
            }
        }
    }

    void BillboardSet::_destroyBuffer()
    {
        if (mpVertexBuffer.notNull())
        {
            GLBufferObjectManager::GetSingleton().removeBySource(mpVertexBuffer->getSource());
            mpVertexBuffer.setNull();
        }
        if (mpIndexBuffer.notNull())
        {
            GLBufferObjectManager::GetSingleton().removeBySource(mpIndexBuffer->getSource());
            mpIndexBuffer.setNull();
        }
        mpVertexData.setNull();
        mpIndexData.setNull();
        mpVertexBufferIterator = 0;
        mpIndexBufferIterator = 0;
    }

    void BillboardSet::_sortBillboards(const Camera *pCamera)
    {
        if (pCamera)
        {
            switch (getSortMode())
            {
                case SM_DISTANCE:
                    BillboardSet::RadixSorter.sort(mActiveBillboards, SortByDistanceFunctor(pCamera->getPosition(TS_WORLD)));
                    break;

                case SM_DIRECTION:
                    BillboardSet::RadixSorter.sort(mActiveBillboards, SortByDirectionFunctor(pCamera->getBack()));
                    break;

                default: break;
            }
        }
    }

    void BillboardSet::_genOriginOffsets(Vector4 &offsets)
    {
        switch (mOriginType)
        {
            case TAT_BOTTOM_LEFT:
            {
                offsets.x = 0.0f;
                offsets.y = 0.0f;
                offsets.z = 1.0f;
                offsets.w = 1.0f;
                break;
            }

            case TAT_BOTTOM:
            {
                offsets.x = -0.5f;
                offsets.y = 0.0f;
                offsets.z = 0.5f;
                offsets.w = 1.0f;
                break;
            }

            case TAT_BOTTOM_RIGHT:
            {
                offsets.x = -1.0f;
                offsets.y = 0.0f;
                offsets.z = 0.0f;
                offsets.w = 1.0f;
                break;
            }

            case TAT_LEFT:
            {
                offsets.x = 0.0f;
                offsets.y = -0.5f;
                offsets.z = 1.0f;
                offsets.w = 0.5f;
                break;
            }

            case TAT_CENTER:
            {
                offsets.x = -0.5f;
                offsets.y = -0.5f;
                offsets.z = 0.5f;
                offsets.w = 0.5f;
                break;
            }

            case TAT_RIGHT:
            {
                offsets.x = -1.0f;
                offsets.y = -0.5f;
                offsets.z = 0.0f;
                offsets.w = 0.5f;
                break;
            }

            case TAT_TOP_LEFT:
            {
                offsets.x = 0.0f;
                offsets.y = -1.0f;
                offsets.z = 1.0f;
                offsets.w = 0.0f;
                break;
            }

            case TAT_TOP:
            {
                offsets.x = -0.5f;
                offsets.y = -1.0f;
                offsets.z = 0.5f;
                offsets.w = 0.0f;
                break;
            }

            case TAT_TOP_RIGHT:
            {
                offsets.x = -1.0f;
                offsets.y = -1.0f;
                offsets.z = 0.0f;
                offsets.w = 0.0f;
                break;
            }

            default: break;
        }
    }

    // TODO: need to fix
    void BillboardSet::_genBillboardAxes(Vector3 &xAxis, Vector3 &yAxis, const Billboard *bb)
    {
        Vector3 cameraDirection = mCurrentCameraOrientation * Vector3::NEGATIVE_UNIT_Z;
        if (mAccurateFacing)
        {
            cameraDirection = bb->position - mCurrentCameraPosition;
            cameraDirection.normalize();
        }

        switch (mBillboardType)
        {
            case BBT_POINT:
            {
                if (mAccurateFacing)
                {
                    xAxis = cameraDirection.crossProduct(mCurrentCameraOrientation * Vector3::UNIT_Y);
                    xAxis.normalize();
                    yAxis = xAxis.crossProduct(cameraDirection);
                    yAxis.normalize();
                }
                else
                {
                    xAxis = mCurrentCameraOrientation * Vector3::UNIT_X;
                    yAxis = mCurrentCameraOrientation * Vector3::UNIT_Y;
                }
                break;
            }

            case BBT_ORIENTED_COMMON:
            {
                const Quaternion &q = _getDerivedOrientation();
                yAxis = q * Vector3::UNIT_Y;
                yAxis.normalize();
                xAxis = cameraDirection.crossProduct(yAxis);
                xAxis.normalize();
                break;
            }

            case BBT_ORIENTED_SELF:
            {
                yAxis = bb->axis;
                yAxis.normalize();
                xAxis = cameraDirection.crossProduct(yAxis);
                xAxis.normalize();
                break;
            }

            case BBT_PERPENDICULAR_COMMON:
            {
                const Quaternion &q = _getDerivedOrientation();
                Vector3 rotateAxis = q * Vector3::UNIT_Y;
                xAxis = cameraDirection.crossProduct(rotateAxis);
                xAxis.normalize();
                yAxis = rotateAxis.crossProduct(xAxis);
                yAxis.normalize();
                break;
            }

            case BBT_PERPENDICULAR_SELF:
            {
                xAxis = cameraDirection.crossProduct(bb->axis);
                xAxis.normalize();
                yAxis = bb->axis.crossProduct(xAxis);
                yAxis.normalize();
                break;
            }

            default: break;
        }
    }

    // Left:x  Bottom:y  Right:z  Top:w
    void BillboardSet::_genVertexOffsets(const real width, const real height,
                                         const Vector4 &originOffsets,
                                         const Vector3 &xAxis,
                                         const Vector3 &yAxis,
                                         Vector3 *vertexOffsets)
    {
        Vector3 left = xAxis * (originOffsets.x * width);
        Vector3 bottom = yAxis * (originOffsets.y * height);
        Vector3 right = xAxis * (originOffsets.z * width);
        Vector3 top = yAxis * (originOffsets.w * height);

        /** TODO: Face
            1----3
            |\   |
            | \  |
            |  \ |
            |   \|
            0----2
        */
        vertexOffsets[0] = bottom + left;
        vertexOffsets[1] = top + left;
        vertexOffsets[2] = bottom + right;
        vertexOffsets[3] = top + right;
    }

    void BillboardSet::_genVertices(const Vector3* const vertexOffsets, const Billboard &bb)
    {
        if (mPointRendering)
        {
            *mpVertexBufferIterator++ = bb.position.x;
            *mpVertexBufferIterator++ = bb.position.y;
            *mpVertexBufferIterator++ = bb.position.z;
            if (mUseColor)
            {
                *mpVertexBufferIterator++ = bb.color.r;
                *mpVertexBufferIterator++ = bb.color.g;
                *mpVertexBufferIterator++ = bb.color.b;
                *mpVertexBufferIterator++ = bb.color.a;
            }
            if (mUseTexCoord)
            {
                *mpVertexBufferIterator++ = bb.texCoords[0].x;
                *mpVertexBufferIterator++ = bb.texCoords[1].y;
            }
        }
        else
        {
            /** TODO: Face
                1----3
                |\   |
                | \  |
                |  \ |
                |   \|
                0----2
            */
            // bottom-left
            *mpVertexBufferIterator++ = bb.position.x + vertexOffsets[0].x;
            *mpVertexBufferIterator++ = bb.position.y + vertexOffsets[0].y;
            *mpVertexBufferIterator++ = bb.position.z + vertexOffsets[0].z;
            if (mUseColor)
            {
                *mpVertexBufferIterator++ = bb.color.r;
                *mpVertexBufferIterator++ = bb.color.g;
                *mpVertexBufferIterator++ = bb.color.b;
                *mpVertexBufferIterator++ = bb.color.a;
            }
            if (mUseTexCoord)
            {
                *mpVertexBufferIterator++ = bb.texCoords[0].x;
                *mpVertexBufferIterator++ = bb.texCoords[0].y;
            }

            // top-left
            *mpVertexBufferIterator++ = bb.position.x + vertexOffsets[1].x;
            *mpVertexBufferIterator++ = bb.position.y + vertexOffsets[1].y;
            *mpVertexBufferIterator++ = bb.position.z + vertexOffsets[1].z;
            if (mUseColor)
            {
                *mpVertexBufferIterator++ = bb.color.r;
                *mpVertexBufferIterator++ = bb.color.g;
                *mpVertexBufferIterator++ = bb.color.b;
                *mpVertexBufferIterator++ = bb.color.a;
            }
            if (mUseTexCoord)
            {
                *mpVertexBufferIterator++ = bb.texCoords[0].x;
                *mpVertexBufferIterator++ = bb.texCoords[1].y;
            }

            // bottom-right
            *mpVertexBufferIterator++ = bb.position.x + vertexOffsets[2].x;
            *mpVertexBufferIterator++ = bb.position.y + vertexOffsets[2].y;
            *mpVertexBufferIterator++ = bb.position.z + vertexOffsets[2].z;
            if (mUseColor)
            {
                *mpVertexBufferIterator++ = bb.color.r;
                *mpVertexBufferIterator++ = bb.color.g;
                *mpVertexBufferIterator++ = bb.color.b;
                *mpVertexBufferIterator++ = bb.color.a;
            }
            if (mUseTexCoord)
            {
                *mpVertexBufferIterator++ = bb.texCoords[1].x;
                *mpVertexBufferIterator++ = bb.texCoords[0].y;
            }

            // top-right
            *mpVertexBufferIterator++ = bb.position.x + vertexOffsets[3].x;
            *mpVertexBufferIterator++ = bb.position.y + vertexOffsets[3].y;
            *mpVertexBufferIterator++ = bb.position.z + vertexOffsets[3].z;
            if (mUseColor)
            {
                *mpVertexBufferIterator++ = bb.color.r;
                *mpVertexBufferIterator++ = bb.color.g;
                *mpVertexBufferIterator++ = bb.color.b;
                *mpVertexBufferIterator++ = bb.color.a;
            }
            if (mUseTexCoord)
            {
                *mpVertexBufferIterator++ = bb.texCoords[1].x;
                *mpVertexBufferIterator++ = bb.texCoords[1].y;
            }

            // Index
            uint32_t indexOffset = uint32_t(mVisibleBillboardNum << 2);
            *mpIndexBufferIterator++ = indexOffset;
            *mpIndexBufferIterator++ = indexOffset + 2;
            *mpIndexBufferIterator++ = indexOffset + 1;
            *mpIndexBufferIterator++ = indexOffset + 3;
            *mpIndexBufferIterator++ = indexOffset + 1;
            *mpIndexBufferIterator++ = indexOffset + 2;
        }

        ++mVisibleBillboardNum;
    }
}
